﻿using Common;
using Linguist.Dictionary;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Util;
using Util.Props;

namespace Linguist.Language.Grammar
{
    /// <summary>
    /// Classes that implement this interface create grammars. A grammar is represented internally as a graph of {@link
    /// GrammarNode GrammarNodes} linked together by {@link GrammarArc GrammarArcs}. Calling {@link #getInitialNode()
    /// getInitialNode} will return the first node of the grammar graph. To traverse the grammar graph, one should call
    /// GrammarNode.getSuccessors, which will return an array of GrammarArcs, from which you can reach the neighboring
    /// GrammarNodes.
    ///
    /// Note that all grammar probabilities are maintained in LogMath log domain.
    /// </summary>
    public abstract class Grammar: IConfigurable, IGrammarInterface
    {
            /** Property to control the the dumping of the grammar */
        [S4Boolean(defaultValue = false)]
        public static String PROP_SHOW_GRAMMAR = "showGrammar";
        /** The default value for PROP_SHOW_GRAMMAR. */

        [S4Boolean(defaultValue = true)]
        public static String PROP_OPTIMIZE_GRAMMAR = "optimizeGrammar";

        /** Property to control whether silence words are inserted into the graph */
        [S4Boolean(defaultValue = false)]
        public static String PROP_ADD_SIL_WORDS = "addSilenceWords";

        /** Property to control whether filler words are inserted into the graph */
        [S4Boolean(defaultValue = false)]
        public static String PROP_ADD_FILLER_WORDS = "addFillerWords";

        /** Property that defines the dictionary to use for this grammar */
        [S4Component(type = typeof(IDictionary))]
        public static String PROP_DICTIONARY = "dictionary";

        // ----------------------------
        // Configuration data
        // -----------------------------
       
        private Boolean _optimizeGrammar = true;
        private Boolean _addSilenceWords;
        private Boolean _addFillerWords;
        protected IDictionary dictionary;
        protected GrammarNode initialNode;
        private List<GrammarNode> grammarNodes;

        private static Word[][] EMPTY_ALTERNATIVE = new Word[0][];
        private Random randomizer = new Random(56); // use fixed initial to make get deterministic random value for testing
        private int maxIdentity;
        private Boolean idCheck;

        public Grammar(Boolean showGrammar,Boolean optimizeGrammar,Boolean addSilenceWords, Boolean addFillerWords, IDictionary dictionary ) 
        {

            this._optimizeGrammar = optimizeGrammar;
            this._addSilenceWords = addSilenceWords;
            this._addFillerWords = addFillerWords;
            this.dictionary = dictionary;
        }

        public Grammar() {

        }

        /*
       /// (non-Javadoc)
        *
       /// @see edu.cmu.sphinx.util.props.Configurable#newProperties(edu.cmu.sphinx.util.props.PropertySheet)
        */
        public void newProperties(PropertySheet ps)
        {

            _optimizeGrammar = ps.getBoolean(PROP_OPTIMIZE_GRAMMAR);

            _addSilenceWords = ps.getBoolean(PROP_ADD_SIL_WORDS);
            _addFillerWords = ps.getBoolean(PROP_ADD_FILLER_WORDS);

            dictionary = (IDictionary) ps.getComponent(PROP_DICTIONARY);
        }


        /** Create the grammar
        /// @throws java.io.IOException*/
        public void allocate()
        {
            dictionary.allocate();
            newGrammar();
            Timer timer = TimerPool.getTimer(this, "grammarLoad");
            timer.start();
            initialNode = createGrammar();
            timer.stop();
        }


        /** Deallocate resources allocated to this grammar */
        public void deallocate() 
        {
            initialNode = null;
            grammarNodes = null;
            dictionary.deallocate();
        }

        /// <summary>
        /// Returns the initial node for the grammar
        /// </summary>
        /// <returns>the initial grammar node</returns>
        public GrammarNode getInitialNode() 
        {
            return initialNode;
        }

        /// <summary>
        ///         
        /// Perform the standard set of grammar post processing. This can include
        /// inserting silence nodes and optimizing out empty nodes
        /// 
        /// </summary>
        protected void postProcessGrammar() 
        {
            if (_addFillerWords) 
            {
                addFillerWords();
            } 
            else if (_addSilenceWords) 
            {
                addSilenceWords();
            }

            if (_optimizeGrammar) 
            {
                optimizeGrammar();
            }
            dumpStatistics();
        }


        /** Dumps statistics for this grammar */
        public void dumpStatistics() 
        {

            //int successorCount = 0;
            //logger.info("Num nodes : " + getNumNodes());
            //for (GrammarNode grammarNode : grammarNodes)
            //    successorCount += grammarNode.getSuccessors().length;

            //logger.info("Num arcs  : " + successorCount);
            //logger.info("Avg arcs  : "
            //        + ((float) successorCount / getNumNodes()));

        }


        /**
        /// Dump a set of random sentences that fit this grammar
         *
        /// @param path  the name of the file to dump the sentences to
        /// @param count dumps no more than this. May dump less than this depending upon the number of uniqe sentences in the
        ///              grammar.
         */
        public void dumpRandomSentences(String path, int count) 
        {
            try {
                List<String> set = new List<String>();
               
                for (int i = 0; i < count; i++) {
                    String s = getRandomSentence();
                    if (!set.Contains(s)) 
                    {
                        set.Add(s);
                        Trace.WriteLine(s);
                    }
                }
            } 
            catch (Exception ioe) 
            {
                Trace.WriteLine("Can't write random sentences to " + path + ' ' + ioe);
            }
        }


        /**
        /// Dump a set of random sentences that fit this grammar
         *
        /// @param count dumps no more than this. May dump less than this depending upon the number of uniqe sentences in the
        ///              grammar.
         */
        public void dumpRandomSentences(int count) 
        {
            List<String> set = new List<String>();
            for (int i = 0; i < count; i++) 
            {
                String s = getRandomSentence();
                if (!set.Contains(s)) 
                {
                    set.Add(s);
                }
            }
            List<String> sampleList = new List<String>(set);
            sampleList = sampleList.OrderBy(x => x).ToList();
            foreach (String sentence in sampleList) 
            {
                Trace.WriteLine(sentence);
            }
        }


        /**
        /// Returns a random sentence that fits this grammar
         *
        /// @return a random sentence that fits this grammar
         */
        public String getRandomSentence() 
        {
            StringBuilder sb = new StringBuilder();
            GrammarNode node = getInitialNode();
            while (!node.isFinalNode()) {
                if (!node.isEmpty()) {
                    IWord word = node.getWord();
                    if (!word.isFiller())
                        sb.Append(word.getSpelling()).Append(" ");
                }
                node = selectRandomSuccessor(node);
            }
            return sb.ToString().Trim();
        }


        /**
        /// Given a node, select a random successor from the set of possible successor nodes
         *
        /// @param node the node
        /// @return a random successor node.
         */
        private GrammarNode selectRandomSuccessor(GrammarNode node) {
            GrammarArc[] arcs = node.getSuccessors();

            // select a transition arc with respect to the arc-probabilities (which are log and we don't have a logMath here
            // which makes the implementation a little bit messy
            if (arcs.Length > 1) 
            {
                double[] linWeights = new double[arcs.Length];
                double linWeightsSum = 0;

                double EPS = 1E-10;

                for (int i = 0; i < linWeights.Length; i++) {
                    linWeights[i] = (arcs[0].getProbability() + EPS) / (arcs[i].getProbability() + EPS);
                    linWeightsSum += linWeights[i];
                }

                for (int i = 0; i < linWeights.Length; i++) {
                    linWeights[i] /= linWeightsSum;
                }


                double selIndex = randomizer.NextDouble();
                int index = 0;
                for (int i = 0; selIndex > EPS; i++) 
                {
                    index = i;
                    selIndex -= linWeights[i];
                }

                return arcs[index].getGrammarNode();

            } else {
                return arcs[0].getGrammarNode();
            }
        }


        /** Dumps the grammar
        /// @param name*/
        public void dumpGrammar(String name) {
            getInitialNode().dumpDot(name);
        }

        /**
        /// returns the number of nodes in this grammar
         *
        /// @return the number of nodes
         */
        public int getNumNodes() {
            return grammarNodes.Count;
        }


        /// <summary>
        /// returns the set of of nodes in this grammar
        /// </summary>
        /// <returns>the set of nodes</returns>
        public List<GrammarNode> getGrammarNodes() 
        {
            return grammarNodes;
        }


        /** Prepare to create a new grammar */
        protected void newGrammar() {
            maxIdentity = 0;
            grammarNodes = new List<GrammarNode>();
            initialNode = null;
        }


        /**
        /// Creates a grammar. Subclasses of grammar should implement this method.
         *
        /// @return the initial node for the grammar
        /// @throws java.io.IOException if the grammar could not be loaded
         */
        protected abstract GrammarNode createGrammar();


        /**
        /// Create class from reference text (not implemented).
         *
        /// @param bogusText dummy variable
        /// @throws NoSuchMethodException if called with reference sentence
         */
        protected GrammarNode createGrammar(String bogusText)
        {
            throw new Exception("Does not create "
                    + "grammar with reference text");
        }


        /**
        /// Gets the dictionary for this grammar
         *
        /// @return the dictionary
         */
        public IDictionary getDictionary() {
            return dictionary;
        }


        /**
        /// Returns a new GrammarNode with the given set of alternatives.
         *
        /// @param identity the id for this node
        /// @param alts     the set of alternative word lists for this GrammarNode
         */
        protected GrammarNode createGrammarNode(int identity, String[][] alts) 
        {
            GrammarNode node;
            IWord[][] alternatives = new IWord[alts.Length][];
            for (int i = 0; i < alternatives.Length; i++) 
            {
                alternatives[i] = new IWord[alts[i].Length];
                for (int j = 0; j < alts[i].Length; j++) 
                {
                    IWord word = getDictionary().getWord(alts[i][j]);
                    // Pronunciation[] pronunciation =
                    // word.getPronunciations(null);
                    if (word == null) {
                        alternatives = EMPTY_ALTERNATIVE;
                        break;
                    } else {
                        alternatives[i][j] = word;
                    }
                }
            }
            node = new GrammarNode(identity, alternatives);
            add(node);

            return node;
        }


        
        /// <summary>
        /// Returns a new GrammarNode with the given single word. If the word is not in the dictionary, an empty node is
        /// created. The grammar id is automatically assigned
        /// </summary>
        /// <param name="word">the word for this grammar node</param>
        /// <returns></returns>
        protected GrammarNode createGrammarNode(String word) 
        {
            GrammarNode node = createGrammarNode(maxIdentity + 1, word);
            return node;
        }

        /**
        /// Creates an empty  grammar node in this grammar. The gramar ID is automatically assigned.
         *
        /// @param isFinal if true, this is a final node
        /// @return the grammar node
         */
        protected GrammarNode createGrammarNode(Boolean isFinal) {
            return createGrammarNode(maxIdentity + 1, isFinal);
        }


        /// <summary>
        /// Returns a new GrammarNode with the given single word. If the word is not in the dictionary, an empty node is
        /// created
        /// </summary>
        /// <param name="identity">the id for this node</param>
        /// <param name="word">the word for this grammar node</param>
        /// <returns></returns>
        protected GrammarNode createGrammarNode(int identity, String word) 
        {
            GrammarNode node;
            IWord[][] alternatives = EMPTY_ALTERNATIVE;
            IWord wordObject = getDictionary().getWord(word);
            // Pronunciation[] pronunciation = wordObject.getPronunciations(null);
            if (wordObject != null) 
            {
                alternatives = new Word[1][];
                alternatives[0] = new Word[1];
                alternatives[0][0] = wordObject;
                node = new GrammarNode(identity, alternatives);
                add(node);
            } 
            else 
            {
                node = createGrammarNode(identity, false);
                Trace.WriteLine("Can't find pronunciation for " + word);
            }
            return node;
        }
        
        /**
        /// Creates a grammar node in this grammar with the given identity
         *
        /// @param identity the identity of the node
        /// @param isFinal  if true, this is a final node
        /// @return the grammar node
         */
        protected GrammarNode createGrammarNode(int identity, Boolean isFinal) {
            GrammarNode node;
            node = new GrammarNode(identity, isFinal);
            add(node);
            return node;
        }


        /**
        /// Adds the given grammar node to the set of nodes for this grammar
         *
        /// @param node the grammar node
        /// @throws Error
         */
        private void add(GrammarNode node) 
        {
            if (node.getID() > maxIdentity) {
                maxIdentity = node.getID();
            }

            // check to see if there is already a node with the given ID.
            if (idCheck) {
                foreach (GrammarNode grammarNode in grammarNodes) 
                {
                    if (grammarNode.getID() == node.getID()) 
                    {
                        throw new Exception("DUP ID " + grammarNode + " and " + node);
                    }
                }
            }

            grammarNodes.Add(node);

        }


        /**
        /// Eliminate unnecessary nodes from the grammar. This method goes through the grammar and looks for branches to
        /// nodes that have no words and have only a single exit and bypasses these nodes.
         */
        private void optimizeGrammar() {
            List<GrammarNode> nodes = getGrammarNodes();
            foreach(GrammarNode node in nodes)
                node.optimize();
        }


        /// <summary>
        /// Adds an optional silence word after every non-filler word in the grammar 
        /// </summary>
        private void addSilenceWords() 
        {
            HashSet<GrammarNode> nodes = new HashSet<GrammarNode>(getGrammarNodes());
            foreach (GrammarNode g in nodes) 
            {
                if (!g.isEmpty() && !g.getWord().isFiller()) 
                {
                    GrammarNode silNode = createGrammarNode(maxIdentity + 1,
                            dictionary.getSilenceWord().getSpelling());

                    GrammarNode branchNode = g.splitNode(maxIdentity + 1);
                    add(branchNode);

                    g.add(silNode, 0.00f);
                    silNode.add(branchNode, 0.0f);
                    silNode.add(silNode, 0.0f);
                }
            }
        }


        /// <summary>
        /// Adds an optional filler word loop after every non-filler word in the grammar 
        /// </summary>
        public void addFillerWords() 
        {
            List<GrammarNode> nodes = new List<GrammarNode>(getGrammarNodes());

            IWord[] fillers = getInterWordFillers();

            if (fillers.Length == 0) 
            {
                return;
            }

            foreach (GrammarNode wordNode in nodes) 
            {
                if (!wordNode.isEmpty() && !wordNode.getWord().isFiller()) 
                {
                    GrammarNode wordExitNode = wordNode.splitNode(maxIdentity + 1);
                    add(wordExitNode);
                    GrammarNode fillerStart = createGrammarNode(false);
                    GrammarNode fillerEnd = createGrammarNode(false);
                    fillerEnd.add(fillerStart, 0.0f);
                    fillerEnd.add(wordExitNode, 0.0f);
                    wordNode.add(fillerStart, 0.0f);

                    foreach (Word filler in fillers) 
                    {
                        GrammarNode fnode = createGrammarNode(maxIdentity + 1, filler.getSpelling());
                        fillerStart.add(fnode, 0.0f);
                        fnode.add(fillerEnd, 0.0f);
                    }
                }
            }
        }


        /**
        /// Gets the set of fillers after filtering out fillers that don't go between words.
         *
        /// @return the set of inter-word fillers
         */
        private IWord[] getInterWordFillers() 
        {
            List<IWord> fillerList = new List<IWord>();
            IWord[] fillers = dictionary.getFillerWords();

            foreach (IWord fillerWord in fillers) 
            {
                if (fillerWord != dictionary.getSentenceStartWord()
                        && fillerWord != dictionary.getSentenceEndWord()) 
                {
                    fillerList.Add(fillerWord);
                }
            }
            return fillerList.ToArray();
        }

    }
}
